#!/usr/bin/env python
#
# Zenmp3 - a YouTube to MP3 Downloader.
#
# Copyright Vitor Sonoki 2012 - 2015 all rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

'''
The Goal here is present an elegant solution to download music to multiple
platforms, at least between different flavors of UNIX for now.

This solution tackles this problem using the Python language. Woot!

Authored by Vitor Sonoki <vesonoki@confidesk.com>

USAGE:
This file should run standalone in most Python instalations, but if
you would like to use the command-line interface, use it as follows:

zenmp3 [URL] [SONGNAME]

You may also run zenmp3 --help for more information.

Version history:

0.1 ALPHA - First useful version - gets options, downloads songs.
0.2 ALPHA - Major improvements in command-line handling, location and
treatment of downloaded and temporary files.
0.3 ALPHA - First serious attempt to create a Windows build.
0.4 ALPHA - The Windows build is now as functional as its Linux counterpart.
0.5 BETA - Bump for integration with rudimentary GUI - works in GTK+!
0.6 BETA - Python3 compatibility issue solved. Now any Python interpreter
will work (at least in Command-line)
0.7 BETA - Windows users now can use a GUI with default installations can
now use a GUI through Tkinter!
0.8 RC-1 - No more interface freezing, a little refactoring cures all. It now
shows the status of the program in the interface itself! No more
dependencies with Zenity.
0.8 RC-2 -  Errors bubble up a little nicer all the way up to the interface.
            The user doesn't have to fire up from the command-line every time
            anymore.
0.9 BETA - Started work on Mac support.
            
TODO: Handle exceptions gracefully. CLI is too cryptic, GUI is too brief.

- Thinking waaaaay ahead, why not enable batch conversion?
'''

VERSION = "0.9 BETA"

import os, sys, webbrowser
import subprocess as sp
from random import randint

if sys.platform == "darwin":
    OS_TYPE = "mac"
elif sys.platform == "win32":
    OS_TYPE = "windows"
else:
    OS_TYPE = "linux"

# GUI toolkit detection
try:
    import gtk
    hasGTK = True
    hasTK = False
    hasgui = True
    guiframe = "GTK"
except ImportError: # No GTK+; Windows, maybe?
    try:
        import Tkinter as tk
        hasGTK = False
        hasTK = True
        hasgui = True
        guiframe = "TK"
    except ImportError: # Python3
        try:
            import tkinter as tk
            hasgui = True
            hasGTK = False
            hasTK = True
            guiframe = "TK"
        except ImportError: # No supported toolkits found. To the CLI.
            sys.stderr.write("Unable to launch GUI. Falling back to command-line mode...\nHave your options ready!\n")
            hasgui = False

if hasgui:
    print("Now riding with %s" % guiframe) 
    
# A few command-line legacy thangs
if len(sys.argv) == 1 and hasgui:
    pass
elif len(sys.argv) == 1:
    # Fallback to CLI and Error: missing arguments
    print("Error: Missing arguments.")
    print("Try %s --help for more information" % sys.argv[0])
    sys.exit(1)
else:
    # We are forcefully running in command-line mode (which MUST work as well!)
    hasgui = False
    if '--help' in sys.argv or '-h' in sys.argv:
        # Display help options and exit
        print("Zenmp3 - a free YouTube to MP3 converter (Python Version)")
        print("Usage: %s [URL] [SONG NAME]" % sys.argv[0])
        print("Downloads the song from [URL] and saves it as [SONG NAME]")
        print("Options are:")
        print("-h, --help       Displays this help message")
        print("-v, --version    Displays the program version\n\n")
        sys.exit(0)
    elif '--version' in sys.argv or '-v' in sys.argv:
        # Display program version and exit
        print("zenmp3 %s" % VERSION)
        sys.exit(0)
    elif len(sys.argv) == 3:
        # Retrieve the given arguments.
        URL = sys.argv[1]
        SONGNAME = sys.argv[2]
    elif len(sys.argv) > 3:
        # Error: too many arguments
        print("Sorry, we can only download one video at a time.")
        sys.exit(1)
    else:
        # Invalid options...
        print("There are invalid options in %s" % sys.argv[1:len(sys.argv)])
        print("Try %s --help for more information" % sys.argv[0])
        sys.exit(1)
    
def validator(dependency):
    """
    This function will assess whether the current machine is able to run
    Zenmp3 or not. Essentially, it checks for three things:
    
    1. Whether youtube-dl exists.
    2. Whether there is some kind of encoding converter or not.
    3. Whether we have a window toolkit to work with.
    
    The third is optional: whether the widget toolkit is compatible or not.
    This, however is under debate. Stay tuned.
    
    UPDATE: if we're using Windows, we need to add an exception - all windows
    Binaries will be shipped within the zenmp3 directory itself by default,
    so there shouldn't be a need to validate, unless the user has messed with
    our files...
    """
    if OS_TYPE == 'windows':
        satisfied = True # We ship with all dependencies, remember?
    elif OS_TYPE == 'linux' or OS_TYPE == 'mac':
        # Truth is I'm not quite sure about how executables are laid out on a
        # Mac. If they follow the POSIX rules, a PATH should be here.
        # Update: yes, they do. Hooray!
        execpath = os.environ['PATH'].split(":")
        for directory in execpath:
            if dependency in os.listdir(directory):
                satisfied = True
                break
            else:
                satisfied = False
    return satisfied

def sanitycheck():
    '''
    This function searches the local UNIX $PATH directories for the 
    dependencies (youtube-dl and ffmpeg).

    It's out of the scope of this function to determine if the versions
    of youtube-dl have been updated.
    '''
    hasYTD = validator("youtube-dl")
    hasFMP = validator("ffmpeg")
    # Ubuntu 14.04 and beyond has ditched ffmpeg for a similar program, avconv,
    # that is found in the libav package. 
    # Perhaps it's worthy to check one more time?
    if not hasFMP:
        hasFMP = validator("avconv")
    return hasYTD and hasFMP

class Download():
    def __init__(self, system, address, target, directory=None):
        self.system = system
        self.address = address
        if target.endswith('.mp3'):
            self.target = target
        else:
            self.target =  target + '.mp3'
        if self.system == 'linux' or self.system == 'mac':
            self.tempfile = ".youtube-dl_%s.flv" % randint(10000, 99999)
            self.targetdir = directory
            if not self.targetdir.endswith('/'):
                self.targetdir += '/'
            os.chdir(self.targetdir)
        elif self.system == "mac":
            # Execute a bunch of Mac-specific code
            print("Hey, Mac support is still fuzzy, but let's see how it goes.")
        elif self.system == "windows":
            # Hint: Windows binaries will all be shipped together
            # in the same directory structure.
            print("Hey, Windows support is still limited, but we're trying our best!")
            self.rootdir = os.path.realpath('.')
            self.tempfile = "_youtube-dl_%s.flv" % randint(10000, 99999)
            self.targetdir = '%s\\Downloads' % os.environ['USERPROFILE']
            os.chdir(self.targetdir)
        self.hasAVC = validator("avconv")
    
    def fetch(self):
        """
        The fetch() method is equivalent to running the following shell commands:
        
        youtube-dl --output=$tempfile --format=18 $youtubeurl
        ffmpeg -i $tempfile -acodec libmp3lame -ac 2 -ab 128k -vn -y "$songname"
        
        I don't think there is any better way to put it (yet), as the youtube-dl script
        is so well-done and complete that it's hard to substitute.
        """
        print("Fetching the song...")
        if OS_TYPE == 'linux' or OS_TYPE == 'mac':
            self.fetcher = sp.Popen(['youtube-dl', '--output=%s' % self.tempfile, '--format=18','%s' % self.address], stdout=sp.PIPE)
            if hasGTK:
                while self.fetcher.poll() == None:
                    # 'None' means not finished; we can update our GUI in the
                    # meantime. Also, the testing of this loop means that the
                    # program will not carry on the next steps until we're done
                    # TODO: Implement a way of telling the user how far we are,
                    # like progress bars, bytes transferred, etc.
                    while gtk.events_pending():
                        gtk.main_iteration()
            else:
                # I still don't know how to implement this GUI updates in Tk.
                # This is the best that they can get for now...
                self.fetcher.wait()
        else:
            self.fetcher = sp.Popen(['%s\\win32\\bin\\youtube-dl.exe' % self.rootdir, '--output=%s' % self.tempfile, '--format=18','%s' % self.address], stdout=sp.PIPE)
            self.fetcher.wait()
        print("Converting the song...")
        if OS_TYPE == 'linux' or OS_TYPE == 'mac':
            if hasGTK:
                interface.statusbar.set_text("Converting the song...")
            elif hasTK:
                interface.statusbar.config(text = "Converting the song...")
            if not self.hasAVC:
                self.converter = sp.Popen(['ffmpeg', '-i', '%s' % self.tempfile, '-acodec', 'libmp3lame', '-ac', '2','-ab', '128k', '-vn', '-y', '%s' % self.target], stdout=sp.PIPE, stderr=sp.PIPE)
            else:
                self.converter = sp.Popen(['avconv', '-i', '%s' % self.tempfile, '-acodec', 'libmp3lame', '-ac', '2','-ab', '128k', '-vn', '-y', '%s' % self.target], stdout=sp.PIPE, stderr=sp.PIPE)

            if hasGTK:
                while self.converter.poll() == None:
                    while gtk.events_pending():
                        gtk.main_iteration()
            else:
                self.converter.wait()
        else:
            self.converter = sp.Popen(['%s\\win32\\bin\\ffmpeg.exe'% self.rootdir, '-i', '%s' % self.tempfile, '-acodec', 'libmp3lame', '-ac', '2','-ab', '128k', '-vn', '-y', '%s' % self.target], stdout=sp.PIPE, stderr=sp.PIPE)
            self.converter.wait()
    def clean(self):
        if OS_TYPE == 'linux' or OS_TYPE == 'mac':
            os.remove(self.targetdir + self.tempfile)
        elif OS_TYPE == 'windows':
            os.remove(self.tempfile)

class Downloader():
    '''
    Our downloader window.
    
    This window will analize the inputs (to as much as it 
    can) to see if the inputs are ok (i.e. if they exist and
    if it's an url with http and such, and then perform the 
    download if possible, otherwise, tell the user to try again.
    '''
    if hasGTK:
        '''
        This is the de facto, standard Zenmp3 interface. This is the 
        interface that should be used in case the machine has more than
        one widget toolkit installed.
        
        Most Linux distributions with GNOME or GTKish environments will use it
        without problem, but occasionally someone with KDE will pop in.
        That's why we have the other fallback toolkits to help us out.
        '''
        def fetcher(self, widget, data=None):
            self.url = self.urlentry.get_text()
            self.filename = self.nameentry.get_text()
            if self.url == '':
                sys.stderr.write("Didn't quite get that.\n")
            elif self.url.startswith('http://') or self.url.startswith('https://'):
                pass
            else:
                self.url = 'http://' + self.url
            
            if self.filename == '' or self.filename == ' ':
                sys.stderr.write("No filename provided!\n")
            elif self.filename.endswith('.mp3'):
                pass
            else:
                self.filename += '.mp3'
            
            if self.filename != "" and self.url != "":
                # We are good to go!
                print(u"The data sent was:\n%s\n%s" % (self.url, self.filename))
                self.statusbar.set_text("Now downloading the video...")
                dl = Download(OS_TYPE, self.url, self.filename, self.destinationpath.get_text())
                self.urlentry.set_text('')
                self.nameentry.set_text('')
                self.fetcherButton.set_sensitive(False)
                dl.fetch()
                if dl.target in os.listdir(dl.targetdir):
                    print('\n\n--> Song "%s" was successfully downloaded to: %s' % (dl.target, dl.targetdir))
                    dl.clean()
                    self.statusbar.set_text("Song downloaded to %s" % self.destinationpath.get_text())
                else:
                    self.statusbar.set_text("There was an error. Your song could not be downloaded.")
                    print("There was an error. Your song could not be downloaded.")
                    print(dl.converter.communicate()[1])
                    self.fetcherButton.set_sensitive(True) # Try again.
                    dl.clean()
            else:
                self.statusbar.set_text("Error: please provide a URL and a file name.")
            
            # Restore everything
            self.fetcherButton.set_sensitive(True)
            
        def delete_event(self, widget, data):
            gtk.main_quit()
            return False

        def cd(self, widget, data=None):
            "Change the saving directory through a GUI selector"
            self.dialog = gtk.FileChooserDialog(
                title = "Choose a target directory",
                parent = self.window,
                action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
                buttons =  (
                    gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                    gtk.STOCK_OK, gtk.RESPONSE_OK
                )
            )
            self.savedirectory = self.dialog.run()
            if self.savedirectory == gtk.RESPONSE_OK:
                print("Destination set to: %s" % self.dialog.get_filename())
                self.destinationpath.set_text(self.dialog.get_filename())
            else:
                print("Folder selection cancelled.")
            self.dialog.destroy()

            
        def __init__(self):
            # Preprocessor variables for the GUI:
            self.elementList = []
            self.savedirectory = os.environ["HOME"]
 
            # Interface overlay
            self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
            self.window.set_title("ZenMP3 - YouTube Music Downloader")
            self.window.set_default_size(440, 120)
            self.window.set_border_width(10)
            self.fetcherButton = gtk.Button("Download song")
            self.elementList.append(self.fetcherButton)
            self.grid = gtk.VBox(5, False)
            self.elementList.append(self.grid)
            self.urlfield = gtk.HBox(0, False)
            self.elementList.append(self.urlfield)
            self.urlentry = gtk.Entry()
            self.elementList.append(self.urlentry)
            self.urllabel = gtk.Label("Video URL:")
            self.elementList.append(self.urllabel)
            self.namefield = gtk.HBox(0, False)
            self.elementList.append(self.namefield)
            self.nameentry = gtk.Entry()
            self.elementList.append(self.nameentry)
            self.namelabel = gtk.Label("Filename:")
            self.elementList.append(self.namelabel)
            self.destinationfield = gtk.HBox(0, False)
            self.elementList.append(self.destinationfield)
            self.destinationlabel = gtk.Label("Destination:")
            self.elementList.append(self.destinationlabel)
            self.destinationpath = gtk.Entry()
            self.destinationpath.set_text(self.savedirectory)
            self.elementList.append(self.destinationpath)
            self.destinationchange = gtk.Button("Change")
            self.elementList.append(self.destinationchange)
            self.statusbar = gtk.Label("Ready")
            self.elementList.append(self.statusbar)
 
            # Set the interaction logic
            self.window.connect("destroy", gtk.main_quit)
            self.fetcherButton.connect("clicked", self.fetcher)
            self.destinationchange.connect("clicked", self.cd)
            
            # Show all elements
            for element in self.elementList:
                element.show()
            
            self.urlfield.pack_start(self.urllabel, False, False)
            self.urlfield.pack_start(self.urlentry, True, True)
 
            self.namefield.pack_start(self.namelabel, False, False)
            self.namefield.pack_start(self.nameentry, True, True)
 
            self.destinationfield.pack_start(self.destinationlabel, False, False)
            self.destinationfield.pack_start(self.destinationpath, True, True)
            self.destinationfield.pack_start(self.destinationchange, False, False)
 
            self.grid.pack_start(self.urlfield, True, False)
            self.grid.pack_start(self.namefield, True, False)
            self.grid.pack_start(self.destinationfield, True, False)
            self.grid.pack_start(self.fetcherButton, False, False)
            self.grid.pack_start(self.statusbar, False, False)
 
            self.window.add(self.grid)
            self.window.show()

        def main(self):
            gtk.main()
    
    elif hasTK:
        '''
        If we can't use GTK, we fall back to the next preferred toolkit.
        And so on if we can't use this toolkit either.
        
        This section is important because sometimes Windows installs 
        cannot make use of PyGTK, even if is installed.
        
        The tkinter class is, therefore, a security blanket to allow
        Windows users with default installations to use the graphical
        user interface
        '''
        def fetcher(self):
            self.url = self.urlfield.get()
            self.filename = self.namefield.get()
            if self.url == '' or self.filename == '':
                sys.stderr.write("Missing arguments: unable to proceed.\n")
                self.statusbar.config(text="Missing arguments.", fg="#F00")
            else:
                if self.url.startswith('http://') or self.url.startswith('https://'):
                    pass
                else:
                    self.url = 'http://' + self.url
                if self.filename.endswith('.mp3'):
                    pass
                else:
                    self.filename += '.mp3'
                self.statusbar.config(text="Now fetching from: %s." % self.url, fg="#000")
                self.urlfield.delete(0, tk.END)
                self.namefield.delete(0, tk.END)
                print(u"The data sent was:\n%s\n%s" % (self.url, self.filename))
                dl = Download(OS_TYPE, self.url, self.filename, os.environ['HOME'])
                dl.fetch()
                if dl.target in os.listdir(dl.targetdir):
                    print('\n\n--> Song "%s" was successfully downloaded to: %s' % (dl.target, dl.targetdir))
                    dl.clean()
                    self.statusbar.config(text="Song '%s' was successfully downloaded to: %s" % (dl.target, dl.targetdir), fg="#05AD2F")
                else:
                    print("There was an error. Your song could not be downloaded.")
                    print(dl.converter.communicate()[1])
                    self.statusbar.config(text="There was an error. Your song could not be downloaded.", fg="#F00")
                    dl.clean()
                
        def givecredit(self):
            print("Authored by Vitor Sonoki - 2013\nhttp://convertmusichere.appspot.com")
            self.statusbar.config(text="Authored by Vitor Sonoki - 2013 - http://convertmusichere.appspot.com", fg="#000")
            
        def onlinehelp(self):
            webbrowser.open('http://convertmusichere.appspot.com/')
            
        def __init__(self):
            # Add overlay
            self.window = tk.Tk()
            self.window.geometry('500x150')
            self.field1 = tk.Frame(self.window)
            self.field2 = tk.Frame(self.window)
            self.label = tk.Label(text="Welcome to Zenmp3.\nTo download a song, please enter an URL and a name for it.")
            self.label2 = tk.Label(self.field1, text="YouTube URL: ")
            self.label3 = tk.Label(self.field2, text="Song name: ")
            self.urlfield = tk.Entry(self.field1)
            self.namefield = tk.Entry(self.field2)
            self.statusbar = tk.Label(self.window, text="")
            self.button = tk.Button(text="Fetch song", command=self.fetcher, bg="#F5CB9A")
            self.credits = tk.Button(text="Credits", command=self.givecredit)
            self.helper = tk.Button(text="Help", command=self.onlinehelp)
            
            # Pack everything neatly
            self.window.title("Zenmp3 - YouTube to MP3 converter - Version %s" % VERSION)
            self.label.pack(expand=1, fill='x')
            self.field1.pack( fill='x')
            self.field2.pack( fill='x')
            self.label2.pack(in_=self.field1, side='left')
            self.urlfield.pack(in_=self.field1, fill="x", side='left', expand=1, padx=5)
            self.label3.pack(in_=self.field2, side='left')
            self.namefield.pack(in_=self.field2, fill="x",side='left', expand=1, padx=5)
            self.statusbar.pack(in_=self.window)
            self.button.pack( side='right', ipadx=5, ipady=5, padx=5, pady=5)
            self.credits.pack( side='left', ipadx=5, ipady=5, padx=5, pady=5)
            self.helper.pack( side='left', ipadx=5, ipady=5, padx=5, pady=5)
            
        def main(self):
            self.window.mainloop()

    else:
        def __init__(self):
            print("Sorry! Yet to implement it on other toolkits!")
            sys.exit(2)

if __name__ == "__main__":
    if hasgui:
        if sanitycheck():
            interface = Downloader()
            interface.main()
        else:
            print("There are unmet dependencies in your computer.\nPlease see http://convertmusichere.appspot.com for more information.")
            sys.exit(1)
    else:
        dl = Download(OS_TYPE, URL, SONGNAME)
        if sanitycheck():
            dl.fetch()
            if dl.target in os.listdir(dl.targetdir):
                print('\n\n--> Song "%s" was successfully downloaded to: %s' % (dl.target, dl.targetdir))
                dl.clean()
            else:
                print("There was an error. Your song could not be downloaded.")
                print(dl.converter.communicate()[1])
                dl.clean()
            sys.exit(0)
        else:
            print("There are unmet dependencies in your computer.")
            print("Please see http://convertmusichere.appspot.com for more information.")
            sys.exit(1)